
package asteroids.model;


import java.lang.Math;
import java.util.*;
//import java.lang.Exception;

//import asteroids.Util;
import asteroids.CollisionListener;
import asteroids.model.programs.Program;
import be.kuleuven.cs.som.annotate.*;



/**
 * A class of ships with a position and a velocity in space and how they interact with each other.
 * 
 * 
 * @invar	The orientation of each ship must be a valid orientation for a ship.
 * 			| 0 <= this.getOrientation() < 2*Math.PI * 
 * @invar	The executable force on the ship is equal to 1,1*10^18 Newton
 * 			| thrustforce == 1.1*10^18
 * @invar	the ship contains only valid bullets
 * 			| this.hasProperBullets()
 * 
 * @version 2.0
 * 
 * @author Thijs Peirelinck & Wouter Parys
 *
 */

public class Ship extends FlyingObject{

	private double orientation;
	private boolean thruster;
	private final double thrustForce;
	private List<Bullet> bulletList;
	private Program program;
	
	/**
	 * Constructor for a ship
	 * 
	 * @param 	positionX
	 * 			The positionX for this new ship.
	 * @param 	positionY
	 * 			The positionY for this new ship.
	 * @param 	velocityX
	 * 			The velocityX for this new ship.
	 * @param 	velocityY
	 * 			The velocityY for this new ship.
	 * @param 	angle
	 * 			The angle for this new ship.
	 * @param 	radius
	 * 			The radius for this new ship.
	 * @param 	mass
	 * 			The mass for this new ship.
	 * @effect	This new ship is initialized as a flying object with the given positions, velocities and mass and a minimal radius equal to 10.
	 * 			| super(positionX,positionY,velocityX,velocityY,mass,10)
	 * @effect	This new ship is initialized with the given angle.
	 * 			| setOrientation(angle)
	 * @effect	This new ship is initialized with the given radius.
	 * 			| setRadius(radius)
	 * @post	The thruster is initialized on false.
	 * 			| this.getThruster() == false
	 * 
	 */
	public Ship(double positionX, double positionY, double velocityX, double velocityY, double angle, double radius, double mass) {
		super(positionX, positionY, velocityX, velocityY, mass, 10);
		setOrientation(angle);
		thruster = false;
		thrustForce = 1.1*Math.pow(10,18);
		setRadius(radius);
		bulletList = new ArrayList<Bullet>();
	}
	
	@Basic @Raw @Immutable
	public double getThrustForce() {
		return thrustForce;
	}
	
	@Basic @Raw
	public boolean getThruster() {
		return thruster;
	}
	
	/**
	 * set the thruster of this ship on or off
	 * @param   thruster
	 * 			the boolean that needs to be set to thruster
	 * @post	the thruster of this ship is equal to the param thruster 
	 * 			|(new this).getThruster() == thruster
	 */
	public void setThruster(boolean thruster) {
		this.thruster = thruster;
	}
	
	@Basic @Raw
	public double getOrientation() {
		return orientation;
	}
	
	/**
	 * Changes the orientation of a ship.
	 * 
	 * @param	orientation
	 * 			the new orientation for this ship.
	 * @pre		the new orientation needs to be equal to or bigger as zero and smaller than 2*Pi
	 * 			| 0 <= orientation < 2*Math.PI
	 * @post	The new orientation of the ship is the same as the given orientation.
	 * 			| (new this).getOrientation() = orientation
	 */
	private void setOrientation(double orientation) {
		assert(orientation >= 0 && orientation < Math.PI);
		this.orientation = orientation;
	}
	
	/**
	 * Turn the ship with a given angle.
	 * 
	 * @param	angle
	 * 			The given angle with which the ship must turn.
	 * @pre		The current orientation is a double equal to or bigger as zero and smaller than 2*Pi.
	 * 			| 0 <= getOrientation() < 2*Math.PI
	 * @post	The new orientation must present the sum of the old orientation with the given angle.
	 * 			because the new orientation must be bigger than or equal to zero and smaller than 2*Pi, 
	 * 			the new orientation is equal to the modulu of this sum. 
	 * 			When the result of this modulu is an orientation smaller than zero, 2*Pi will be added.
	 * 			| if ( (this.getOrientation() + angle) % (2*Math.PI) >= 0)
	 * 			|		then (new this).getOrientation == ((this.getOrientation() + angle) % (2*Math.PI))
	 * 			| if ( (getOrientation() + angle) % (2*Math.PI) < 0)
	 * 			|		then (new this).getOrientation == (((this.getOrientation() + angle) % (2*Math.PI)) + 2*Math.PI)
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 */
	public void turn(double angle) throws IllegalStateException {
		if(isTerminated()) { throw new IllegalStateException(); }
		double theta =( (this.getOrientation()+angle) % (2*Math.PI));
		if(theta<0) {
			theta = theta + 2*Math.PI;
		}
		setOrientation(theta);
		
	}
	
	/**
	 * Change the velocity of the ship depending on a given acceleration factor.
	 * 
	 * @param	a
	 * 			The given acceleration factor.
	 * @param	dt
	 * 			the given time-interval
	 * @pre		The given acceleration factor is a number in the range of the class Double.
	 * @post	the ship accelarated 
	 * 			| if(a > 0) then a=0
	 * 				| (new this).getVelocityX() = this.getVelocityX() + a*Math.cos(this.getOrientation())
	 * 				| (new this).getVelocityY() = this.getVelocityY() + a*Math.cos(this.getOrientation())
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 */
	public void thrust(double a, double dt) throws IllegalStateException {
		if(isTerminated()) { throw new IllegalStateException(); }
		if(a < 0) {
			a=0;
		}
		setVelocity(this.getVelocityVector().getX() + a*Math.cos(this.getOrientation()),this.getVelocityVector().getY() + a*Math.sin(this.getOrientation()));

	}
	
	/**
	 * Collide this ship with another flyingObject.
	 * 
	 * @param	flyingObject
	 * 			The flyingObject that will collide with this ship.
	 * @param	collisionListener
	 * 			The interface that takes care of a potential collision in the GUI.
	 * @effect	If and only if flyingObject is an instance of Ship, this ship and the flyingObject
	 * 			will bounce off each other.
	 * 			| if (flyingObject instanceof Ship)
	 * 			|	then super.bounceoff(flyingObject)
	 * @effect	If and only if flyingObject is an instance of Asteroid, this Ship will terminate
	 * 			and a collision will occur.
	 * 			| if (flyingObject instanceof Asteroid)
	 * 			|	then this.terminate()
	 * 			|		 collisionListener.objectCollision(this, flyingObject, this.getCollisionPosition(flyingObject)[0], this.getCollisionPosition(flyingObject)[1])
	 * @effect	If and only if flyingObject is an instance of Bullet, the collision will be solved in Bullet.
	 * 			| if (flyingObject instanceof Bullet)
	 * 			|	then flyingobject.collide(this,collisionListener)
	 *@throws	IllegalArgumentException
	 *			| when the flyingObject is in an other world
	 *			| this.getWorld() != flyingObject.getWorld()
	 *@throws	IllegalArgumentException
	 *			when this or flyingObject doesn't have a world
	 *			| (!this.hasWorld()) || (!flyingObject.hasWorld())
	 *@throws	NullPointerException
	 *			| when flyingObject is the null-reference
	 *			| flyingObject == null
	 *@throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 */
	@Override
	public void collide(FlyingObject flyingObject, CollisionListener collisionListener) throws IllegalArgumentException, IllegalStateException, NullPointerException  {
		if(isTerminated()) { throw new IllegalStateException(); }
		if(flyingObject.getWorld() != this.getWorld() || !this.hasWorld() || !flyingObject.hasWorld()) { throw new IllegalArgumentException();} 
		if (flyingObject instanceof Ship){
			super.bounceoff(flyingObject);
		}
		
		if (flyingObject instanceof Asteroid){
			this.terminate();
			collisionListener.objectCollision(this, flyingObject, this.getCollisionPosition(flyingObject)[0], this.getCollisionPosition(flyingObject)[1]);
		}
		
		if (flyingObject instanceof Bullet) {
			((Bullet)flyingObject).collide(this,collisionListener);
		}
	}
	
	/**
	 * Move a ship with a given dt.
	 * 
	 * @param	dt
	 * 			The time the ship will move.
	 * @effect	The ship will move over a time dt. 
	 * 			| super.move(dt)
	 * @effect	If and only if the thruster is activated, the ship will thrust with a value
	 * 			that is calculated by dividing the thrustForce of the ship by the mass of the ship
	 * 			and multipy this result with dt.
	 * 			|this.thrust((this.getThrustForce()/this.getMass())*dt,dt)
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 * 			
	 */
	@Override
	public void move(double dt) throws IllegalStateException  {
		if(isTerminated()) { throw new IllegalStateException(); }
		super.move(dt);
		if (thruster == true) {
			this.thrust((this.getThrustForce()/this.getMass())*dt,dt);
		}
	}
	
	/**
	 * makes this ship fire a bullet
	 * 
	 * @throws	NullPointerException
	 * 			This ship does not have a world.
	 * 			| !super.hasWorld() 
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 * @post	if the ship is able to fire a bullet within a world then the ship fires a bullet.
	 * 				if not, the effect will be solved in World, dependent on the reason why the bullet can not be fired.
	 * 			if this ship has less than 3 bullets fired then the ship can fire antoher bullet
	 * 			| bulletList.size() < 2	
	 * 			| let bullet be new Bullet(this.getPositionX()+(this.getRadius())*Math.cos(this.getOrientation()), this.getPositionY()+(this.getRadius())*Math.sin(this.getOrientation()),this.getVelocityX()+250*Math.cos(this.getOrientation()),this.getVelocityY()+250*Math.sin(this.getOrientation()),3,this)
	 * 			| if this.getWorld().canHaveAsFlyingObject(bullet)
	 * 			|	then this.getWorld.getBulletList.contains(bullet) == true and bulletList.contains(bullet) == true
	 * 			| else
	 * 			|	then this.getWorld.solveImpossibleBullet(bullet)
	 * 
	 */
	public void fire() throws NullPointerException, IllegalStateException {
		if(isTerminated()) { throw new IllegalStateException(); }
		Bullet bullet = new Bullet(this.getPositionX()+(this.getRadius()+3.5)*Math.cos(this.getOrientation()), this.getPositionY()+(this.getRadius()+3.5)*Math.sin(this.getOrientation()),this.getVelocityX()+250*Math.cos(this.getOrientation()),this.getVelocityY()+250*Math.sin(this.getOrientation()),3,this);
		if (this.getWorld().canHaveAsFlyingObject(bullet)) {
			if(bulletList.size() <= 2) {
				this.getWorld().addFlyingObject(bullet);
				this.addBullet(bullet);
			}
		}
		else {
			this.getWorld().solveImpossibleBullet(bullet);
		}
		
		
	}
	
	/**
	 * Check whether a bullet is a bullet of this ship.
	 * 
	 * @param   flyingObject
	 * 			The bullet that needs to be checked.
	 * @return	true if the flyingObject is a bullet of this ship
	 * 			| bulletList.contains(flyingObject)
	 */
	public boolean isBulletOff(FlyingObject flyingObject){
		return bulletList.contains(flyingObject);
	}
	

	/**
	 * Add a new bullet to the ship list of bullets.
	 * 
	 * @param	bullet
	 * 			the bullet that has to be add
	 * @post	the bulletList contains the given bullet
	 * 			| bulletList.contains(bullet)
	 * @throws	IllegalArgumentException
	 * 			| when this ship can't have that bullet as bullet
	 * 			| ! this.canHaveAsBullet
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 */
	private void addBullet(Bullet bullet) throws IllegalArgumentException, IllegalStateException {
		if(isTerminated()) { throw new IllegalStateException(); }
		if(! this.canHaveAsBullet(bullet)) { throw new IllegalArgumentException(); }
		bulletList.add(bullet);
	}
	
	/**
	 * Check whether this ship can have the given bullet as bullet.
	 * 
	 * @param bullet
	 * @return	true if this ship can have the bullet
	 * 			result == (!bullet.isTerminated() && !bulletList.contains(bullet))
	 */
	@Model
	private boolean canHaveAsBullet(Bullet bullet) {
		return (bullet != null && !bullet.isTerminated() && !bulletList.contains(bullet) && bullet.getSource()==this);
	}
	
	/**
	 * Check whether this ship has proper bullets.
	 * 
	 * @return	true if this ship has proper bullets
	 * 			| if(for each bullet in bulletList canHaveAsbullet(bullet) == true) 
	 * 			|	then result == true
	 * 			| else result == false;
	 * 
	 */
	public boolean hasProperBullets() {
		for(Bullet bullet: bulletList) {
			if(!this.canHaveAsBullet(bullet)) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Remove the given bullet.
	 * 
	 * @param	bullet
	 * 			| the bullet that has to be removed
	 * @post	the given bullet is not in bulletList anymore
	 * 			bulletList.contains(bullet) == false
	 * @throws	IllegalArgumentException
	 * 			| throws an IllegalArgumentException when the given bullet is null
	 * 			| bullet == null
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 */
	public void removeBullet(Bullet bullet) throws IllegalArgumentException, IllegalStateException {
		if(isTerminated()) { throw new IllegalStateException(); }
		if(bullet == null) {throw new IllegalArgumentException(); }
		bulletList.remove(bullet);
	}
	
	/**
	 * removes the bullet with the given index
	 * 
	 * @param	index
	 * 			the index which has to be removed
	 * @post	the given index 
	 * 			bulletList.remove(index)
	 * @throws	indexOutOfBoundsException
	 * 			when the index is to large or to small
	 * @throws	IllegalStateException
	 * 			when this object is terminated
	 * 			| isTerminated()
	 */
	public void removeBullet(int index) throws IllegalStateException, IndexOutOfBoundsException {
		if(isTerminated()) { throw new IllegalStateException(); }
		bulletList.remove(index);
	}
	
	/**
	 * Set a program to this ship.
	 * 
	 * @param	program
	 * 			| the program you want to load this ship with
	 * @post	this ship has a program
	 * 			| this.hasProgram()
	 * @post	the program of this ship is the given program
	 * 			| (new this).getProgram() == program
	 * @throws	IllegalStateException
	 * 			when this ship is terminated
	 * 			this.isTerminated()
	 */
	public void setProgram(Program program) throws IllegalStateException {
		if(this.isTerminated()) { throw new IllegalStateException(); }
		this.program = program;
	}
	
	/**
	 * Check whether this ship has a program.
	 * 
	 * @return	true if this ship has a program
	 * 			| result == (this.getProgram != null)
	 */
	@Raw
	public boolean hasProgram() {
		return this.getProgram() != null;
	}
	
	@Basic @Raw
	public Program getProgram() {
		return program;
	}
}
